/*****************************************************************************************
   
   Copyright (c) Microsoft Corporation. All rights reserved.

   Use of this code sample is subject to the terms of the Microsoft
   Permissive License, a copy of which should always be distributed with
   this file.  You can also access a copy of this license agreement at:
   http://www.microsoft.com/resources/sharedsource/licensingbasics/permissivelicense.mspx

 ****************************************************************************************/

using System;

namespace CommandUtils
{
	/// <summary>
	/// Base class for commandline utils
	/// </summary>
	public class UtilBase
	{
		protected bool m_Verbose;

		protected string[] m_Arguments;
		protected bool[] m_ArgumentUsed;

		protected System.Xml.XmlDocument m_StandardInputXmlDocument = null;
		protected System.Xml.XmlDocument m_ConfigurationFileXmlDocument = null;

		private System.IO.StreamReader m_StandardInputStreamReader;
		public System.IO.StreamWriter m_StandardOutputStreamWriter;
		public System.IO.StreamWriter m_StandardErrorStreamWriter;

		protected string m_UsageMessage = "Usage message goes here";
		protected string m_HelpMessage = "Help goes here";

		public enum CheckResult
		{
			Succeeded = 0,
			ShowHelp = 1,
			ShowUsage = 2,
			Failed = 0xFF
		}

		public UtilBase(string[] i_Arguments)
		{
			m_Arguments = i_Arguments;
			m_ArgumentUsed = new bool[m_Arguments.Length];
		}

		public bool Verbose 
		{ 
			get 
			{
				return m_Verbose; 
			} 
		}

		public System.IO.StreamReader StandardInput 
		{
			get
			{
				return m_StandardInputStreamReader; 
			}	
		}

		public void SetStandardInput(System.IO.StreamReader i_StreamReader, bool i_Console)
		{
			m_StandardInputStreamReader = i_StreamReader;
			
			if (!i_Console)
			{
				//  Read the input stream as an XML document
				//  NOTE:  Don't do this if we're running with un-redirected console input because
				//  the app will sit waiting for the user to press ctrl+Z
				try
				{
					m_StandardInputXmlDocument = new System.Xml.XmlDocument();
					m_StandardInputXmlDocument.LoadXml(i_StreamReader.ReadToEnd());
				}
				catch( Exception e )
				{
					throw new Exception( "There is an error in the input-stream XML.", e );
				}
			}
		}

		public System.IO.StreamWriter StandardOutput
		{
			get
			{
				return m_StandardOutputStreamWriter;
			}
			set
			{
				m_StandardOutputStreamWriter = value;
			}
		}

		public System.IO.StreamWriter StandardError
		{
			get
			{
				return m_StandardErrorStreamWriter;
			}
			set
			{
				m_StandardErrorStreamWriter = value;
			}
		}

		public string UsageMessage
		{
			get
			{
				return m_UsageMessage;
			}
			set
			{
				m_UsageMessage = value;
			}
		}

		public string HelpMessage
		{
			get
			{
				return m_HelpMessage;
			}
			set
			{
				m_HelpMessage = value;
			}
		}

		private string GetCommandlineParameter( string i_ParameterName, bool i_Flag )
		{
			string ParameterValue = null;
			int Index = 0;

			while (Index < m_Arguments.Length)
			{
				string Argument = m_Arguments[Index];
				int ArgumentIndex = Argument.ToLower().IndexOf(i_ParameterName.ToLower());

				bool ArgumentFound =
					(ArgumentIndex == 0) || 
					((ArgumentIndex == 1) && ((Argument[0] == '/') || (Argument[0] == '-'))) || 
					((ArgumentIndex == 2) && (Argument.StartsWith("--")));

				if (ArgumentFound)
				{
					if (i_Flag && (Argument.Length == ArgumentIndex + i_ParameterName.Length))
					{
						ParameterValue = "";
						m_ArgumentUsed[Index] = true;
						break;
					}
					else if ((Argument.Length > ArgumentIndex + i_ParameterName.Length) && (Argument[ArgumentIndex + i_ParameterName.Length] == '='))
					{
						ParameterValue = Argument.Substring(ArgumentIndex + i_ParameterName.Length + 1);
						m_ArgumentUsed[Index] = true;
						break;
					}
				}

				++Index;
			}
			return ParameterValue;
		}

		/// <summary>
		/// Read a (string) parameter.  Returns null if that parameter is not specified.
		/// </summary>
		/// <param name="i_ParameterName">Parameter name</param>
		/// <returns></returns>
		public string GetParameterValue(string i_ParameterName)
		{
			return GetParameterValue
				(
				i_ParameterName,
				false
				);
		}

		public string GetParameterValue(string i_ParameterName, bool i_Flag)
		{
			// Return string value of the parameter (null if not found)
			// If "i_Flag", empty-string is returned for "/GetParameterValue" not just "/GetParameterValue=xxx"

			// Look at the commandline first
			string ParameterValue = GetCommandlineParameter( i_ParameterName, i_Flag );

			// If nothing was specified on the commandline, look at the stdin xml document
            // NOTE - ParameterValue could be "", which means there was input!!! (just no parameter)
			if ((ParameterValue == null) && (m_StandardInputXmlDocument != null))
			{
				System.Xml.XmlElement el = (System.Xml.XmlElement)m_StandardInputXmlDocument.SelectSingleNode("//Parameters/Parameter[@Name='" + i_ParameterName + "']");
				if (el != null)
				{
					ParameterValue = el.GetAttribute( "Value" );
					if( i_Flag && ParameterValue==null )
					{
						ParameterValue = "";	// flags don't need any value; we return non-null to indicate the flag is present
					}
				}
			}

			// If no hit yet, look at the redirected configuration settings document
            // NOTE - ParameterValue could be "", which means there was input!!! (just no parameter)
            if ((ParameterValue == null) && (m_ConfigurationFileXmlDocument != null))
			{
				System.Xml.XmlElement el = (System.Xml.XmlElement)m_ConfigurationFileXmlDocument.SelectSingleNode("//Parameters/Parameter[@Name='" + i_ParameterName + "']");
				if (el != null)
				{
					ParameterValue = el.GetAttribute( "Value" );
					if( i_Flag && ParameterValue==null )
					{
						ParameterValue = "";	// flags don't need any value; we return non-null to indicate the flag is present
					}
				}
			}

			// If no hit yet, look at the configuration settings
            // NOTE - ParameterValue could be "", which means there was input!!! (just no parameter)
            if (ParameterValue == null)
				ParameterValue = System.Configuration.ConfigurationManager.AppSettings.Get(i_ParameterName.ToLower());
			
			return ParameterValue;
		}

		/// <summary>
		/// Query presence of a (boolean) parameter.  Flag parameters don't need any value.
		/// </summary>
		/// <param name="i_ParameterName">Parameter name</param>
		/// <returns></returns>
		public bool GetParameterValueFlag(string i_ParameterName)
		{
			return (GetParameterValue(i_ParameterName, true) != null);
		}

		/// <summary>
		/// Read a (int) parameter,  Returns -1 if not found.
		/// </summary>
		/// <param name="i_ParameterName">Parameter name</param>
		/// <returns></returns>
		public int GetParameterValueInt(string i_ParameterName)
		{
			return GetParameterValueInt(i_ParameterName, -1);
		}

		/// <summary>
		/// Read a (int) parameter.
		/// </summary>
		/// <param name="i_ParameterName">Parameter name</param>
		/// <param name="i_DefaultValue">Default value to be returned if the parameter was not specified.</param>
		/// <returns></returns>
		public int GetParameterValueInt(string i_ParameterName, int i_DefaultValue)
		{
			object ParameterValue = GetParameterValue(i_ParameterName);
			
			if (ParameterValue != null)
			{
				try
				{
					ParameterValue = int.Parse((string)ParameterValue);
				}
				catch (System.Exception)
				{
					ParameterValue = i_DefaultValue;
				}
			}
			else
				ParameterValue = i_DefaultValue;

			return (int)ParameterValue;
		}

		/// <summary>
		/// Read a parameter, returning a single XmlNode with the parameter contents (or null).
		/// This doesn't read from commandline, only from stdin or configuration file
		/// where the parameter is specified as
		///		&lt;Parameters&gt;&lt;Parameter Name="xxx"&gt;...value...&lt;Parameter&gt&lt;/Parameters&gt
		/// </summary>
		/// <param name="i_ParameterName">Parameter name</param>
		/// <returns></returns>
		public System.Xml.XmlNode GetParameterValueNode(string i_ParameterName)
		{
			System.Xml.XmlNode XmlNode = null;
			
			if (m_StandardInputXmlDocument != null)
				XmlNode = m_StandardInputXmlDocument.SelectSingleNode("//Parameters/Parameter[@Name='" + i_ParameterName + "']");
			
			if ((XmlNode == null) && (m_ConfigurationFileXmlDocument != null))
				XmlNode = m_ConfigurationFileXmlDocument.SelectSingleNode("//Parameters/Parameter[@Name='" + i_ParameterName + "']");

			return XmlNode;
		}

		/// <summary>
		/// Read all parameters by name, returning XmlNodeList.
		/// </summary>
		/// <param name="i_ParameterName">Parameter name</param>
		/// <returns></returns>
		public System.Xml.XmlNodeList GetParameterValueNodes(string i_ParameterName)
		{
			System.Xml.XmlNodeList XmlNodeList = null;
			
			if (m_StandardInputXmlDocument != null)
				XmlNodeList = m_StandardInputXmlDocument.SelectNodes("//Parameters/Parameter[@Name='" + i_ParameterName + "']");

			if ((XmlNodeList == null) && (m_ConfigurationFileXmlDocument != null))
				XmlNodeList = m_ConfigurationFileXmlDocument.SelectNodes(("//Parameters/Parameter[@Name='" + i_ParameterName + "']"));
			
			return XmlNodeList;
		}

		/// <summary>
		/// Read all parameters by name, returning an enumeration of their string values.
		/// </summary>
		/// <param name="i_ParameterName">Parameter name</param>
		/// <returns></returns>
		public System.Collections.IEnumerable GetParameterValueEnum(string i_ParameterName)
		{
			string[] ParameterValues = null;

			System.Xml.XmlNodeList XmlNodeList = GetParameterValueNodes(i_ParameterName);
			if (XmlNodeList != null)
			{
				ParameterValues = new string[XmlNodeList.Count];
				for (int Index = 0; Index < XmlNodeList.Count; ++Index)
				{
					System.Xml.XmlElement XmlElement = (System.Xml.XmlElement)XmlNodeList.Item(Index);
					if (XmlElement.HasAttribute("Value"))
					{
						ParameterValues[Index] = XmlElement.GetAttribute("Value");
					}
					else
					{
						ParameterValues[Index] = XmlElement.InnerText;
					}
				}
			}
			else
			{
				// Nothing found in the config file.  Check commandline too.
				string ParameterValue = GetCommandlineParameter(i_ParameterName, false);
				if (!System.String.IsNullOrEmpty(ParameterValue))
				{
					ParameterValues = new string[1];
					ParameterValues[0] = ParameterValue;
				}
			}

			return ParameterValues;
		}

		// Check that the parameter string is Ok
		// If not, display appropriate error message or help message, and return false
		public virtual UtilBase.CheckResult CheckParameters()
		{
			// Check "help"
			if (GetParameterValueFlag("help") || GetParameterValueFlag("?"))
				return UtilBase.CheckResult.ShowHelp;

			// Check "redirect the configuration file"
			string ConfigurationFile = GetParameterValue("X");
			if (!System.String.IsNullOrEmpty(ConfigurationFile))
			{
				if (!System.IO.File.Exists(ConfigurationFile))
					throw new System.Exception("The configuration file '" + ConfigurationFile + "' was not found.");
				
				try
				{
					m_ConfigurationFileXmlDocument = new System.Xml.XmlDocument();
					m_ConfigurationFileXmlDocument.Load(ConfigurationFile);
				}
				catch( Exception e )
				{
					throw new Exception( "There is an error in the configuration file '" + ConfigurationFile + "'", e );
				}
			}

			// Check "verbose"
			m_Verbose = GetParameterValueFlag("verbose") || GetParameterValueFlag("V");

			return UtilBase.CheckResult.Succeeded;
		}

		public virtual UtilBase.CheckResult CheckUnusedParameters()
		{
			// Any commandline GetParameterValueeters which have not been used?
			char[] SpaceChars = { ' ', '\r', '\n', '\a', '\b', '\f', '\t', '\v' };
			for (int Index = 0; Index < m_Arguments.Length; ++Index)
			{
				if(!m_ArgumentUsed[Index] && m_Arguments[Index].ToString().Trim(SpaceChars)!="" )
				{
					DisplayUsage("The parameter '" + m_Arguments[Index] + "' was not understood.");
					return UtilBase.CheckResult.Failed;
				}
			}

			return UtilBase.CheckResult.Succeeded;
		}

		public System.Collections.IList UnusedParameters
		{
			get
			{
				char[] SeparatorChars = { '=' };
				char[] TrimChars = { '-', '/' };
				char[] SpaceChars = { ' ', '\r', '\n', '\a', '\b', '\f', '\t', '\v' };
				System.Collections.ArrayList Parameters = new System.Collections.ArrayList();
				for (int Index = 0; Index < m_Arguments.Length; ++Index)
				{
					if (!m_ArgumentUsed[Index] && m_Arguments[Index].ToString().Trim(SpaceChars)!="" )
					{
						string[] Pairs = m_Arguments[Index].Split(SeparatorChars, 2);
						string ArgumentName = Pairs[0].TrimStart(TrimChars);
						Parameters.Add(ArgumentName);
					}
				}
				return Parameters;
			}
		}

		public virtual UtilBase.CheckResult Run()
		{
			return UtilBase.CheckResult.Failed;
		}

		public virtual void DisplayUsage()
		{
			DisplayUsage(null);
		}

		public virtual void DisplayUsage(string i_Message)
		{
			if (m_StandardErrorStreamWriter == null)
				return;

			if (!System.String.IsNullOrEmpty(i_Message))
				m_StandardErrorStreamWriter.WriteLine(i_Message);

			m_StandardErrorStreamWriter.WriteLine("Usage: " + m_UsageMessage);
			m_StandardErrorStreamWriter.WriteLine("Try '" + System.Environment.GetCommandLineArgs()[0] + " /?' for more information.");
		}
		
		public virtual void DisplayHelp()
		{
			m_StandardErrorStreamWriter.WriteLine(m_HelpMessage);
		}


		// RunProcess
		// - Runs a process which uses the same semantics as we do
		//   passing its parameter block as stdin
		// - Returns an XML document
		// - In the case of an error code returned frome the app, this will throw an exception containing the errortext.

		// RunProcess( "my.exe", "/Param=Value", "/Param=Value", "etc" )
		public System.Xml.XmlDocument RunProcess(string i_Path, params object[] i_Arguments)
		{
			char[] SeparatorChars = { '=' };
			char[] TrimChars = { '-', '/' };
			System.Collections.Hashtable Hashtable = new System.Collections.Hashtable(i_Arguments.Length);
			foreach (string Argument in i_Arguments)
			{
				string[] Pairs = Argument.Split(SeparatorChars, 2);
				string ArgumentName = Pairs[0].TrimStart(TrimChars);
				string ArgumentValue = (Pairs.Length > 1) ? Pairs[1] : "";
				Hashtable.Add(ArgumentName, ArgumentValue);
			}

			return RunProcess
				(
				i_Path, 
				Hashtable,
				null 
				);
		}

		// RunProcess with a string/object hashtable of argument names/values
		// Where values are XML elements, they'll be serialized inside a "Parameter" element content.
		public System.Xml.XmlDocument RunProcess(string i_Path, System.Collections.IDictionary i_ArgumentTable)
		{
			return RunProcess
				(
				i_Path, 
				i_ArgumentTable,
				null
				);
		}

		// RunProcess with a hashtable AND an XML document.
		// Anything in the hashtable will be appended the document's existing structure.
		public System.Xml.XmlDocument RunProcess(string i_Path, System.Collections.IDictionary i_ArgumentTable, System.Xml.XmlDocument i_XmlDocument)
		{
			// If there is no XMLDocument, construct one.
			if (i_XmlDocument == null)
				i_XmlDocument = new System.Xml.XmlDocument();

			if (i_XmlDocument.DocumentElement == null)
				i_XmlDocument.LoadXml("<Parameters/>");

			System.Xml.XmlElement ParametersXmlElement = (System.Xml.XmlElement)i_XmlDocument.SelectSingleNode("//Parameters");
			if (ParametersXmlElement == null)
			{
				ParametersXmlElement = i_XmlDocument.CreateElement("Parameters");
				i_XmlDocument.DocumentElement.AppendChild(ParametersXmlElement);
			}

			// If we were given a table of parameters, add them as children to the "Parameters" element.
			// (We don't pass them on the real commandline, so we can handle very long parameter values)
			foreach (string ArgumentName in i_ArgumentTable.Keys)
			{
				object ArgumentValue = i_ArgumentTable[ArgumentName];
				System.Xml.XmlElement ParameterXmlElement = i_XmlDocument.CreateElement("Parameter");
				ParameterXmlElement.SetAttribute("Name", ArgumentName);

				if (ArgumentValue is string)
					ParameterXmlElement.SetAttribute("Value", (string)ArgumentValue);
				else if (ArgumentValue is System.Xml.XmlNode)
					ParameterXmlElement.AppendChild(((System.Xml.XmlNode)ArgumentValue).Clone());
				else
					throw new ArgumentException(ArgumentName + " is of unhandled type.");

				ParametersXmlElement.AppendChild(ParameterXmlElement);
			}

			// We return an XmlDocument
			// which will be null if the process put nothing in stdout
			System.Xml.XmlDocument ResultXmlDocument = null;

			System.Diagnostics.Process Process = new System.Diagnostics.Process();
			int ExitCode=2;

			try
			{
				Process.StartInfo.UseShellExecute = false;
				Process.StartInfo.RedirectStandardInput = true;
				Process.StartInfo.RedirectStandardOutput = true;
				Process.StartInfo.RedirectStandardError = true;
				Process.StartInfo.CreateNoWindow = true;
				Process.StartInfo.FileName = i_Path;
				Process.Start();
				
				// Write our parameter document to the child process stdin stream
				i_XmlDocument.Save(Process.StandardInput);
				Process.StandardInput.Flush();
				Process.StandardInput.Close();

				//  We can't just block indefinitely because of the nature of redirected output stream
				//  which requires stream to be cleared from time to time or both parent and child will
				//  block.  In this case we wait 1 second between stream reading.
				System.Text.StringBuilder StdOutContent = new System.Text.StringBuilder();
				System.Text.StringBuilder StdErrContent = new System.Text.StringBuilder();
				while (!Process.HasExited)
				{
					StdOutContent.Append( Process.StandardOutput.ReadToEnd() );
					StdErrContent.Append( Process.StandardError.ReadToEnd() );
					Process.WaitForExit(1000);
				}
				StdOutContent.Append( Process.StandardOutput.ReadToEnd() );
				StdErrContent.Append( Process.StandardError.ReadToEnd() );

				ExitCode = Process.ExitCode;
				Process.Close();

				// If the process returned a non-zero exit code, that's an error.
				if (ExitCode != 0)
				{
					// Ignore the stdout stream.
					// Throw an exception with the contents of the stderr stream.
					StdErrContent.AppendFormat("Error status code {0}", ExitCode);
					throw new Exception(StdErrContent.ToString());
				}

				// Pipe the stderr stream to our own stderr stream (hey, why not?)
				StandardError.WriteLine(StdErrContent);

				if (StdOutContent.Length > 0)
				{
					// Construct an XML document by parsing the stdout value.
					// Note that our protocol defines a process' stdout value as always being well-formed XML (this will throw if it isn't).
					ResultXmlDocument = new System.Xml.XmlDocument();
					ResultXmlDocument.LoadXml(StdOutContent.ToString());
				}
			}
			catch (System.Exception Exception)
			{
				string Message = String.Format("Failure running {0}", i_Path);
				throw new System.Exception(Message, Exception);
			}

			return ResultXmlDocument;
		}
	}
}
